using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Net.Sockets;
using System.Net.Security;
using System.IO;
using System.Security.Cryptography;
using System.Security.Authentication;

using System.Security.Cryptography.X509Certificates;
using Crypt = System.Security.Cryptography;

namespace fuzzyKittenVNC
{
	
	public class IncompatibleClientException : Exception {};
	public class InvalidHeaderException : Exception {};
	public class VersionNotImplementedException : Exception {};
	public class CommunicationErrorException : Exception {};	
	public class WebSocketClosedException : Exception {};	

	/// <summary>
	/// Class that handles Websocket communication with browser 
	/// </summary>
	/// <example>
	/// WebSocketClient client = new WebSocketClient(socket,false,WebSocketClient.PROTO_VERSION.DRAFT_03,0xffffffff);
	/// client.receiveHandler += vnc.requestUpdateFromRequest; // call requestUpdateFromRequest(byte[] req) which handles received data
	/// vnc.renderer.sendDelegate += client.sendVoid; // sendVoid(byte[]) will be used to send data
	/// 
	/// </example>
	public class WebSocketClient : ObservableSubject, ISocketObservable,ICommunicator
	{
		public enum ConnectionStatus {
			NOT_STARTED 	= 0x0100,
			PROTOCOL_SEND	= 0x0101,
			SENDING_HEADERS = 0x0102,
			FINISHED_HEADERS= 0x0103,
			WAITING_FOR_RESP= 0x0204,
			ESTABLISHED     = 0x02fe,
			ERROR 	 		= 0x02ff
		}
		
		public enum ConnectionMask {
			WAITING = 0x0100,
			FINISHED = 0x0200
		}
		
		public enum PROTO_VERSION {
			DRAFT_00 = 0x0000,	//http://www.ietf.org/id/draft-ietf-hybi-thewebsocketprotocol-00.txt
			DRAFT_03 = 0x0001	//http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-03
		}
		
		
	
		public static X509Certificate serverCertificate;
		public onReceive receiveHandler;
		public ConnectionStatus status = ConnectionStatus.NOT_STARTED;
		public WebSocketHeader header;
		protected long max_frame_size = 0x7fffffff;
		protected PROTO_VERSION websocket_protocol = PROTO_VERSION.DRAFT_03;
		protected Socket socket;
		protected Stream stream;
		protected bool useTLS = false;
		protected int aliveCounter = -1;
		
		
		public onReceive getReceiveHandler() {
			return this.receiveHandler;
		}
		/// <summary>
		/// Constructor
		/// 
		/// </summary>
		/// <param name="socket">
		/// A <see cref="Socket"/> through which to communicate via the WebSocket protocol
		/// </param>
		/// <param name="useTLS">
		/// A <see cref="System.Boolean"/> that indicates if communication should be encrypted
		/// </param>
		/// <param name="protocol">
		/// A <see cref="PROTO_VERSION"/> that determines which Version to use.
		/// Due to the missing implementation of DRAFT_03 in current Browsers (March 2011),
		/// DRAFT_03 is not tested yet
		/// </param>
		/// <param name="max_frame_size">
		/// A <see cref="System.Int64"/> that determines the maximum size a Websocket frame may have
		/// </param>
		/// 
		public WebSocketClient (Socket socket, bool useTLS, PROTO_VERSION protocol,long max_frame_size)
		{
			this.socket = socket;
			this.useTLS = useTLS;	
			NetworkStream tmp_stream = new NetworkStream(socket);
			if(this.useTLS) {
				Logger.log("Using TLS in websocket",Logger.LEVEL.INFO);
				SslStream sslStream = new SslStream(tmp_stream,true);
				try {
					sslStream.AuthenticateAsServer(WebSocketClient.serverCertificate,false,SslProtocols.Tls,true);
				} catch(Exception e) {
					Console.WriteLine("{0}",e.Message);
					if (e.InnerException != null)
                	{
                	    Console.WriteLine("Inner exception: {0}", e.InnerException.Message);
                	}
					throw(e);
				}
				Logger.log("TLS Connection established",Logger.LEVEL.DEBUG);
				this.stream = sslStream;
			} else {
				this.stream = tmp_stream;
			}
			this.websocket_protocol = protocol;
			this.performHandshake();
			
			this.max_frame_size = max_frame_size;
			System.Threading.Thread child = 
				new System.Threading.Thread(new System.Threading.ThreadStart(this.receiveThread));
			child.Start();
		}
		
		/// <summary>
		/// Handles packets received during handshake phase
		/// </summary>
		/// <param name="bufferBytes">
		/// A <see cref="System.Byte[]"/> 
		/// </param>
		/// <param name="size">
		/// A <see cref="System.Int32"/>
		/// </param>
		protected void updateHandshake(byte[] bufferBytes, int size) {
			string buffer = System.Text.Encoding.UTF8.GetString(bufferBytes);
			System.IO.StringReader stream = new System.IO.StringReader(buffer);
			string requestLine = stream.ReadLine();
			while(requestLine != null) {
				Logger.log(String.Format("{0}",requestLine),Logger.LEVEL.DEBUG);
		
				switch(this.status) {
					case ConnectionStatus.NOT_STARTED:
						this.parseProtocolLine(requestLine);
						break;
					case ConnectionStatus.PROTOCOL_SEND:
					case ConnectionStatus.SENDING_HEADERS:
						this.parseHeader(requestLine);
						break;
					case ConnectionStatus.FINISHED_HEADERS:
						this.readKey(bufferBytes,size);
						break;
				}
				requestLine = stream.ReadLine();
			}
		}
		
		/// <summary>
		/// Reads the protocol line (GET mojadev.de..) and updates ressource
		/// </summary>
		/// <param name="line">
		/// A <see cref="System.String"/>
		/// </param>
		protected void parseProtocolLine(string line) {
			Regex ressourceRegExp = new Regex("^GET (?<RESSOURCE>.*?) HTTP/1.1");
			Match match = ressourceRegExp.Match(line);
			if(match.Groups.Count < 2)
				throw new InvalidHeaderException();
			this.header.ressource = match.Groups["RESSOURCE"].Value;
			this.status = ConnectionStatus.PROTOCOL_SEND;
		
		}
		
		/// <summary>
		/// Parses the header variables send during the handshake
		/// </summary>
		/// <param name="line">
		/// A <see cref="System.String"/>
		/// </param>
		protected void parseHeader(string line) {
			Match match;
			if(line == "") {
				this.status = ConnectionStatus.FINISHED_HEADERS;	
			} else {
				Regex customVariable = new Regex("(?<HEADERNAME>.*?): (?<HEADERVALUE>.*)");
				match = customVariable.Match(line);
				if(match.Groups.Count == 3)
					this.header.addHeader(match.Groups["HEADERNAME"].Value,match.Groups["HEADERVALUE"].Value);
			}
		}
		
		/// <summary>
		/// Reads the handshake key from the request line
		/// </summary>
		/// <param name="line">
		/// A <see cref="System.Byte[]"/>
		/// </param>
		/// <param name="size">
		/// A <see cref="System.Int32"/>
		/// </param>
		protected void readKey(byte[] line,int size) {
			for(int i=0;i<8;i++)
				this.header.keyAppendix[i] = line[size-8+i];
			
			this.status = WebSocketClient.ConnectionStatus.WAITING_FOR_RESP;
		}
		
		/// <summary>
		/// Checks wheter the header contains two valid keys
		/// </summary>
		/// <returns>
		/// A <see cref="System.Boolean"/> determining whether the header is valid  
		/// </returns>
		public bool isValid() {
			if(this.header.getHeader("Sec-WebSocket-Key1").Count < 1)
				return false;
			if(this.header.getHeader("Sec-WebSocket-Key2").Count < 1)
				return false;
			return true;
		}
		
		/// <summary>
		/// Computes the respond challenge key as described in the draft
		/// </summary>
		/// <returns>
		/// A <see cref="System.Byte[]"/> representing the challenge
		/// </returns>
		protected byte[] computeResponse() {
			byte[] key1_raw = Encoding.UTF8.GetBytes((string) this.header.getHeader("Sec-WebSocket-Key1")[0]);
			byte[] key2_raw = Encoding.UTF8.GetBytes((string) this.header.getHeader("Sec-WebSocket-Key2")[0]);
			uint key1 = this.computeKey(key1_raw);		
			uint key2 = this.computeKey(key2_raw);			

			byte[] challenge = new byte[16];	

			// concatenate in big endian
			for(int i =0;i<4;i++) {
				challenge[i] =  Convert.ToByte((key1>>(24-i*8))&0xff); 
				challenge[i+4] = Convert.ToByte((key2>>(24-i*8))&0xff);
				challenge[i+8] = (byte)  header.keyAppendix[i];
				challenge[i+12] = (byte)  header.keyAppendix[i+4];
			}
			
			Crypt.MD5 md5 = Crypt.MD5.Create();
			return md5.ComputeHash(challenge);
		}
		
		/// <summary>
		/// Compute key as described in the draft
		/// </summary>
		/// <param name="str">
		/// A <see cref="System.Byte[]"/> Websocket key
		/// </param>
		/// <returns>
		/// A <see cref="System.UInt32"/> substituted response key for use in the challeng algorithm
		/// </returns>
		protected uint computeKey(byte[] str) {
			uint target = 0;
			uint spaceCount = 0;
			for(int i=0;i<str.Length;i++) {
				if(str[i] >= 48 && str[i]<=57) {
					if(target>0) 
						target *= 10;
					target += str[i]-(uint)48;
				} else if(str[i] == ' ')
					spaceCount++;
			}
			
			return target /= spaceCount;
		}		
		
		/// <summary>
		/// Sends data via the websocket protocol
		/// </summary>
		/// <param name="data">
		/// A <see cref="System.String"/>
		/// </param>
		/// <returns>
		/// A <see cref="System.Int64"/> how many bytes were send 
		/// </returns>
		public long send(string data) {
			if(this.status != ConnectionStatus.ESTABLISHED)	
				return -1;
			return this.send(Encoding.UTF8.GetBytes(data),false);
			
		}
		
		public void sendVoid(byte[] data) {
			this.send(data,false);
		}
		
		public void sendVoid(byte[] data,bool binary) {
			this.send(data,binary);
		}
		
		
		public long send(byte[] data) {
			//if(this.stream.CanWrite)
				return this.send(data,false);	
			//else 
			// throw new WebSocketClosedException();
		}
		/// <summary>
		/// Sends data via the determined protocol
		/// </summary>
		/// <param name="data">
		/// A <see cref="System.Byte[]"/> containing the data to send 
		/// </param>
		/// <param name="binary">
		/// A <see cref="System.Boolean"/> determing if it's binary data
		/// </param>
		/// <returns>
		/// A <see cref="System.Int64"/> of how many bytes were send
		/// </returns>
		public long send(byte[] data,bool binary) {
			if(!this.socket.Connected) {
				this.notify();
				return -1;
			}
			switch(this.websocket_protocol) {
				case PROTO_VERSION.DRAFT_00:
					return this.send_early_implementation(data,binary);			
				case PROTO_VERSION.DRAFT_03:
					return this.send_draft_03(data,binary);
				default:
					throw new VersionNotImplementedException();
			}
		}
		
		private long send_draft_03(byte[] data,bool binary) {
			long offset = 0;
			
			do {
				long length = (data.LongLength-offset)%this.max_frame_size;
				byte[] subFrameData = new byte[length];
				Array.Copy(data,offset,subFrameData,0,length);			
				offset += length;
				
				WebSocketFrame frame = new WebSocketFrame();
				frame.moreFlag = offset<data.LongLength;	
				if(offset<data.LongLength)
					frame.opcode = WebSocketFrame.OPCODES.OPCODE_CONTINUE;
				else
					frame.opcode = (binary) ? WebSocketFrame.OPCODES.OPCODE_BINARY : WebSocketFrame.OPCODES.OPCODE_TEXT_FRAME; 
				frame.application_data = subFrameData;
			} while(offset<data.LongLength);
			return offset;
		}

		private long send_early_implementation(byte[] data,bool binary) {		
			byte[] frame = new byte[data.LongLength+2];
			frame[0] = 0x00;
			Array.Copy(data,0,frame,1,data.LongLength);
			frame[data.LongLength+1] = 0xff;
			this.stream.Write(frame,0,frame.Length);		
			return frame.Length;
		}
			
		/// <summary>
		/// Entry point for receive thread
		/// </summary>
		public void receiveThread() {
			try {
				int nrOfBytes;
				byte[] inBuffer = new byte[this.socket.ReceiveBufferSize];
				byte[] received;
				for(;;) {
					if(this.socket == null)
						throw new WebSocketClosedException();
					if(!this.socket.Connected) {
						throw new WebSocketClosedException();
						break;
					}
					nrOfBytes = this.stream.Read(inBuffer,0,this.socket.ReceiveBufferSize);						
					
					if(nrOfBytes > 0) {
						received = new byte[nrOfBytes-1];
						Array.Copy(inBuffer,1,received,0,nrOfBytes-1);		
						this.receiveHandler(received);
						aliveCounter = -1;
					} else if(this.aliveCounter > -1) {
						if(this.aliveCounter == 0) {
							this.socket.Close();
						}
						this.aliveCounter--;
					} else {
						string ping = "PING";
						this.send(Encoding.UTF8.GetBytes(ping));
						this.aliveCounter = 10;
					}
					inBuffer = new byte[this.socket.ReceiveBufferSize];
				}
			} catch(Exception e) {
				this.notify();
			}
		}
		/// <summary>
		/// sends disonnect package
		/// </summary>
		public void disconnect() {
			byte[] frame = {0x01,0x00,0xff};
			this.stream.Write(frame,0,3);	
			this.shutdown();
		}	
	
		/// <summary>
		/// Starts handshake with client
		/// </summary>
		protected void performHandshake() {	
			this.header = new WebSocketHeader(WebSocketHeader.HEADER_TYPE.HANDSHAKE_REQUEST);			
			this.receiveHandShakeRequest();
			if(this.socket.Connected)
				this.openingHandshake();
		}
		
		/// <summary>
		/// receive Handshake data
		/// </summary>
		protected void receiveHandShakeRequest() {
			Logger.log("Starting to receive handshake",Logger.LEVEL.DEBUG);
			
			byte[] buffer = new byte[4096];
			int size = 0;
				
			while(((int) this.status &  (int) ConnectionMask.FINISHED) == 0) {
				if(this.socket.Connected && this.stream.CanRead)
					size = this.stream.Read(buffer,0,4096);
				else 
					size = -1;	
				if(size < 0)
					break;
				
				this.updateHandshake(buffer,size);
			}
			Logger.log("Received handshake",Logger.LEVEL.DEBUG);
			
			if(!this.isValid())
				throw new IncompatibleClientException();
		}
				
		/// <summary>
		/// Send Websocket HTTP update header 
		/// </summary>
		private void openingHandshake() {
			byte[] key = this.computeResponse();
			WebSocketHeader responseHeader = new WebSocketHeader(WebSocketHeader.HEADER_TYPE.HANDSHAKE_RESPONSE);
			responseHeader.addHeader("Upgrade","WebSocket");
			responseHeader.addHeader("Connection","Upgrade");	
			responseHeader.keyAppendix = key;
			
			WebSocketResponse response = new WebSocketResponse(responseHeader,null,this.useTLS);
			response.fillFromRequestHeader(header);
			this.stream.Write(response.getHeaderString(),0,response.getHeaderString().Length);
//			this.socket.Send(response.getHeaderString());
			// TOOO: Websocket PING in order to check connectivity
			this.status = WebSocketClient.ConnectionStatus.ESTABLISHED;
		}
		
		public override Object getStatus() {
			return (Object) this.socket.Connected;
		}
		
		public void shutdown() {
			this.stream.Close();
			this.socket.Close();
		}
		
	}
}

